Node.js 异步 I/O
Node 还有 异步/IO、事件驱动、单线程等特点,其中 “单线程” 是指程序的主线程,也就是将所有阻塞的部分交给线程池进行处理,之后一个队列跟线程池进行协作。
对于 JavaScript Code 不再需要过多关注线程的问题,主要通过 callback
回调构成,当程序运行时主线开始不停的循环适中调用。
至于 node.js 所采用的单线程异步非阻塞模式,就是在异步非阻塞模式执行的过程中,I/O 操作不会阻塞后面的程序执行或计算,当完成 I/O 时,以事件的方式通知,继续执行。
异步 I/O
异步 I/O 其实就是用户空间中的程序不用依赖内核空间中的 I/O 即可操作完成并进行下一步操作,以同步作为对比来讲,假设两个任务的时间按分别为 m
和 n
,如果程序采用同步的方式完成此操作则需要的任务时间为 m+n
。
但如果是采用异步方式处理程序,则可以在两种 I/O 并行的情况下完成任务的时间最大限度的不超过本身的处理最大限度,即同步完成任务的时间 max(m,n)
运行空间
在操作系统中,程序的运行空间(又被称之为核心空间)主要分为内核空间(Kernel space)和用户空间,以 Linux 系统对自身进行划分为例,对于内核空间,是指对硬件设备具有访问权限,这个空间一部分核心软件独立于普通的应用程序,运行在最高或较高的权限级别中。
于此相对的还有一个用户空间(User space),在操作系统中,用户空间又被称之为 “虚拟内存” 或者 “使用者空间” 与核心空间共为运行空间中的两个区块
同样以 Linux 系统作为例子,除了对硬件设备具有访问的通常被称之为内核空间外,对于普通应用程序这部分的,就被称之为用户空间。
必要性
以编程语言为例,为了设计的让应用程序调用方便,将程序设计为同步的 I/O 模型,这就意味着程序中的后续任务都需要等待 I/O 的完成。从而在等待的过程中无法充分的利用 CUP。
但为了充分利用 CPU 以使 I/O 可以并行,可通过实现多线程、单线程以及单线成多进程来达到目的。
多线程与单进程
多线程主要的设计理念就是为了共享在程序空间中实现并行处理任务,从而达到充分利用 CPU 空间的效果,但缺点就是在执行时上下文交换开销较大和同步的问题,但这样会让程序变得复杂化。
单线程与多进程
此方法用于解决多线程与单进程使用的问题,有的语言为了单线程保持了调用的简单化和用户体验,采用多进程的方式来达到充分利用 CPU 和提升总体并行处理能力的行为,但缺点在于当应用程序稍微复杂时,因为业务逻辑无法分布多个进程之间,所以事物的处理能力较高。
但在设计程序时应该选择前者,后者相对与性能若后与前者,且没有优化的余地,而前者可以通过优化来解决开销较大和同步的问题。
还有一个非常主要的问题,在分布式系统或通过 API 方式来处理数据,因此数据的传递很难保持同步的状态,且干扰原因有很多,因此如果网络速度有边,则 m
和 n
的值都会变大,这时候同步的 I/O 语言就会露出弱势。
在分布式系统场景下,异步 I/O 将会体现出优势max(m,n)
的时间最大开销可以有效的缓解值增长所带来的问题。
I/O 线程和状态
对于计算机内核 I/O 而言,同步与异步是线程之间的关系,而阻塞、非阻塞则是对于某一个时刻中的一个状态,线程只能是阻塞或异步的状态,要么线程处于阻塞状态或非阻塞状态。
I/O 的阻塞与非阻塞
阻塞模式的 I/O 自如其名会造成应用程序的等待,直到阻塞完成,同时操作系统也将 I/O 操作设置为非阻塞模式.
在设置非阻塞模式时,可能在应用程序调用时没有拿到真正数据就返回了,为此需要多次调用才能确认 I/O 操作完成
I/O 的同步和异步
如果做阻塞的 I/O 调用,应用程序的等待的调用完成的过程就是同步状态,反而 I/O 为非阻塞模式时,应用程序将是异步的。
异步 I/O 轮询
在进行非阻塞 I/O 调用时,需要读到完整的数据,应用程序需要多次进行论寻,才可确保读取数据的完成,并进行下一步操作
缺点在于应用程序需要主动调用,会造成占用较多的 CUP 时间片,性能较为低下
read
read 论寻技术用于同步非阻塞 I/O 模型,每次论需只能获取一个 I/O 操作对应的状态,轮询操作由用户进行负责,可通过 read()
系统调用,即用户线程需要在一定间隔内非阻塞的持续发起论寻请求来获取 I/O 操作当前状态,所以他是由用户所负责的。
虽然用户线程不会被阻塞,但他需要被不停的触发来引起系统调用,直到用户数据可用为止,因此他是一个较为原始且性能较低(含数据吞吐量较低)且想对于阻塞式系统调用,消耗了额外的 CPU 资源。
I/O 多路复用
I/O 多路复用(I/O Multiplexing)是为实现和满足用户线程不需要进行不停的轮询,以避免消耗额外的 CPU资源,同时在数据就绪时,用户线程处于休眠状态(sleep)。
而当一个或多个文件描述符对应的 I/O 操作数据准备完成时,用户线程将会被唤醒来处理这些 I/O 操作,目前包括 select、poll、epoll 等都是采用多路服用技术来提高性能。
文件描述符是一个非负整数,实际上是一个索引值,指向内核为没一个进程所维护的该进程打开文件的记录表,当程序打开一个现有的文件爱你或创建新文件时,该内核向进程返回一个文件描述符。
select
select 是一种 read 改进方案,也是一种 I/O 多路复用模型,通过对文件描述符上的事件状态来判断。可以让内核在多个文件描述符对应的 I/O 操作中任何一个完成或经过指定时间后唤醒(wake)并通知用户线程。
在唤醒之前用户线程因为被阻塞而处于休眠状态。
poll
poll 是 select 基础之上进行的改进,并改善了不需要的检查,但相对与文件描述符较多的时候,性能还是较为底下的,通过 poll 实现的轮询与 select 较为相似,但性能和限制都有所改善。
epoll
epool 为解决 select 和 poll 中每一次检查监听的文件描述符变化时,都需要遍历一次用户传入的描述符集合,当文件描述符较多时,一次集合的遍历时间会非常耗时的问题。
为了解决这个问题,epool 在 select 和 poll 上的基础进行优化在 Linux kernel 2.6 中引入了 epoll(event poll)方法,他将文件描述符的检测和真正的检测进行了分离。
从实现的角度上来说 epoll 会创建一个 epoll 实例,之后 epoll 实例会床将一个对应的文件描述符(这个 epoll 实例中包含了文件描述符的集合,被称之为了 epoll set
\ interest list
)。
集合中存储了所有希望被监听的文件描述符,当某个描述符对应的 I/O 操作时,文件描述符又会被放入 ready list
中
ready list 是 epoll set 的子集
kqueue\IOCP\event ports
kqueue
kqueue 方法实现与 epoll 类似,不过他仅仅在 FreeBSD 系统下存在,但 macOS 也会使用
IOCP
IOCP 方法相当与 Windows 下的 epoll
event ports
event ports 相当与 Solaris 下的 epoll
⬅️ Go back